package board;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JComponent;
import javax.swing.JFrame;

import board.animation.Animation;
import board.animation.AnimationAutomater;
import board.animation.AnimationGroup;
import board.animation.render.AnimatedOccupantRender;
import board.animation.render.SpinningOccupantRender;
import board.animation.tranlations.CompoundAnimation;
import board.animation.tranlations.LinearTranslation;
import board.animation.tranlations.Translation;
import board.grid.Direction;
import board.grid.Grid;
import board.grid.RangeGrid;
import board.grid.Space;
import board.pieces.BasicPiece;
import board.pieces.Piece;
import board.renders.*;
import board.renders.adapters.CellRenderAdapter;
import board.renders.adapters.LineRenderAdapter;
import board.renders.adapters.OccupantRenderAdapter;
import board.renders.basics.*;

import static java.lang.Math.abs;

/** A simple display for the <code>Grid</code>
 *  in the Swing environment.  
 *  
 *  Modifying the contents displayed by this
 *  class are manipulated by manipulating the
 *  contents of the <code>Grid<T></code> or
 *  by adding/changing the renders used.  
 *  
 *  *This Class does NOT extend JFame - it is
 *  not a window.  To use this, you will need
 *  to add it to some High-level element such
 *  as a JApplet, JDialog, or JFrame.  
 *  
 *  *This Class is NOT thread save - please
 *  only manipulate it in the AWT-EVENT-THREAD
 *  after it has been displayed.  
 * 
 * @author R.Wisniewski1101
 */
public class GridPane extends JComponent implements PointSpaceTranslator {
	private static final long serialVersionUID = 5672289885297310743L;
	/** The grid being displayed by this component.  
	 *  This grid must be set in the constructor
	 *  so that the preferred size can be calculated.  
	 *  This allows for the container that holds
	 *  this component to use <code>pack()</code>
	 *  to put his component at the default
	 *  resolution of 64 px per space.  
	 */
	private Grid<?> grid;
	/** The renders for the occupant classes.  
	 *  Each render is tied to an occupant class
	 *  (which can include interfaces) such
	 *  renders can be added to full utilize
	 *  the properties of that class, and such
	 *  that special render rules can be applied
	 *  for new classes specifically.  
	 *  
	 *  The other registeredOccupantRenders Map
	 *  contains only the Class<?> keys registered
	 *  by the add methods.  This Map will also contain
	 *  other classes for the purpose of caching.  
	 */
	private Map<Class<?>, OccupantRender<?>> occupantRenders;
	/** The render used for any occupant who
	 *  has no applicable renders in the map.  
	 */
	private BasicOccupantRender bor;
	/** The render set for the background.  
	 *  By default, this is the Basic
	 *  version which is just a white background
	 *  area.  
	 */
	private BoardBGRender background;
	/** This is the render used to draw the
	 *  grid lines.  By default they are GRAY.  
	 */
	private LineRender lines;
	/** This is the render used to render the
	 *  background for the cells.  This may seem
	 *  redundant because there is a general
	 *  background image, but this image can
	 *  be different for each cell, and can be
	 *  null (none).  
	 */
	private CellRender cells;
	/** The current dimension of the cells.  
	 *  This is recalculated each time this
	 *  component is redrawn.  
	 */
	private int currentCellSize;
	/** The amount of space to add on the
	 *  left and right of the grid.  
	 */
	private int padding_x;
	/** The amount of space to add on the
	 *  top and bottom of the grid.  
	 */
	private int padding_y;
	/** The animations registered in this pane.  
	 */
	private List<Animation<?>> animations;
	/** The width when the cache was made.  
	 */
	private int width;
	/** The height when the cache was made.  
	 */
	private int height;
	/** The cache of the background with no
	 *  occupants rendered.  
	 */
	private Image unoccupiedCacheImage;
	/** A cache of the points for the spaces
	 *  on the grid.  
	 */
	private Map<Space, Point> spaceCache;
	/** The rendered for occupants as registered
	 *  by the add method.  
	 */
	private Map<Class<?>, OccupantRender<?>> registeredOccupantRenders;
	/** Create a GridPane to display
	 *  the specified <code>Grid<?></code>.   
	 * 
	 * @param grid - the <code>Grid<?></code> to display.  
	 */
	public GridPane (Grid<?> grid) {
		//store the grid
		this.grid = grid;
		//create the renders map
		occupantRenders = new HashMap<Class<?>, OccupantRender<?>>();
		registeredOccupantRenders = new HashMap<Class<?>, OccupantRender<?>>();
		//create the default renders
		bor = new BasicOccupantRender();
		lines = new BasicLineRender();
		cells = new BasicCellRender();
		background = new BasicBoardBGRender();
		//prime the default renders;
		bor.prime(this);
		lines.prime(this);
		cells.prime(this);
		background.prime(this);
		//set the default cell size
		currentCellSize = 64;
		animations = new ArrayList<Animation<?>>();
		spaceCache = new HashMap<Space, Point>();
	}
	/** Get the current width, in pixels, used by the grid.  
	 * 
	 * @return the width of the grid.  
	 */
	public int getGridWidth() {return grid.getWidth() * (currentCellSize + 1) -  1;}
	/** Get the current height, in pixels, used by the grid.  
	 * 
	 * @return the height of the grid.  
	 */
	public int getGridHeight() {return grid.getHeight() * (currentCellSize + 1) - 1;}
	/** Add a render for a specific class.  
	 * 
	 * @param type - the class to which to apply the render
	 * @param render - the render to apply
	 */
	public void setOccupantRender(Class<?> type, OccupantRender<?> render) {
		registeredOccupantRenders.put(type, render);
		occupantRenders.clear();
		occupantRenders.putAll(registeredOccupantRenders);
	}
	/** Change the render that creates the
	 *  background image for the board.  
	 * 
	 * @param render - the new render for the background
	 */
	public void setBoardRender(BoardBGRender render) {background = render;}
	/** Change the render that creates the
	 *  line images for the board.  
	 *   
	 * @param render - the new render for the lines
	 */
	public void setLineRender(LineRender render) {lines = render;}
	/** Change the render that creates the
	 *  background for the cells on the board. 
	 * 
	 * @param render - the new cell render.  
	 */
	public void setCellRender(CellRender render) {cells = render;}
	/** Add a new animation to the pane.  This
	 *  animation will be used to display the
	 *  occupant it returns in its getOccupant()
	 *  method.  
	 * 
	 * @param anim - the animation to add
	 */
	public void addAnimation(Animation<?> anim) {animations.add(anim);}
	/** Remove the animation registered to the given occupant.  
	 * 
	 * @param occupant - the occupant for which to remove the animation.  
	 */
	public void removeAnimation(Object occupant) {animations.remove(getAnimation(occupant));}
	@Override //JComponent
	public void paintComponent(Graphics g) {
		//if there is no grid to draw, stop.  
		if(grid == null)
			return;
		//ensure that the parent class is prepared to draw.  
		this.revalidate();
		/* Up-cast because we need more methods than Graphics has.  
		   This should always work because Graphics2D is used by
		   swing but Graphics is the parameter for AWT compatibility.  
		 */
		Graphics2D g2 = (Graphics2D) g;
		super.paintComponent(g2);
		if(width != getWidth() || height != getHeight() || unoccupiedCacheImage == null) {
			spaceCache.clear();
			width = getWidth();
			height = getHeight();
			unoccupiedCacheImage = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
			Graphics2D cacheG = (Graphics2D) unoccupiedCacheImage.getGraphics();
			//calculate the cell size based on the width
			int w = abs(width / grid.getWidth());
			//calculate the cell size based on the height
			int h = abs(height / grid.getHeight());
			//use the smaller size to ensure that the whole grid is visible
			currentCellSize = w < h ? w : h;
			//if the size is 0, stop because there is no room to draw.  
			if(currentCellSize <= 0)
				return;
			//calculate padding
			padding_x = (width - grid.getWidth() * currentCellSize) / 2;
			padding_y = (height - grid.getHeight() * currentCellSize) / 2;
			//draw background
			cacheG.drawImage(background.getBG(getGridWidth(), getGridHeight()), padding_x, padding_y, this);
			//draw cells
			displayCells(cacheG);
			//draw grid lines
			if(lines.isVerticalFirst()) {
				displayVerticalLines(cacheG);
				displayHorizontalLines(cacheG);
			} else {
				displayHorizontalLines(cacheG);
				displayVerticalLines(cacheG);
			}
			cacheG.dispose();
		}
		//draw the cache
		g2.drawImage(unoccupiedCacheImage, 0, 0, this);
		//draw occupants
		displayOccupants(g2);
	}
	@Override //PointSpaceTranslator
	public Space pointToSpace(Point p) {
		if(p.x >= padding_x && p.x <= padding_x + getGridWidth() && p.y >= padding_y && p.y <= padding_y + getGridHeight()) {
			Space s = Space.get(
				((int)p.getX() - padding_x) / currentCellSize + grid.getMinCol(),
				grid.getHeight() - ((int)p.getY() - padding_y) / currentCellSize + grid.getMinRow()
			);
			return grid.isValidSpace(s) ? s : null; }
		else return null;
	}
	@Override //PointSpaceTranslator
	public Point spaceToPoint(Space s) {
		if(spaceCache.containsKey(s))
			return spaceCache.get(s);
		else {
			Point newVal = spaceToPoint_withOffsets(s, 0.0, 0.0);
			spaceCache.put(s, newVal);
			return newVal;
		}
	}
	@Override //PointSpaceTranslator
	public Point spaceToPoint_withOffsets(Space s, double x_shift, double y_shift) {
		return grid.isValidSpace(s) ? new Point(
				padding_x + (s.getColumn() - grid.getMinCol()) * currentCellSize + (int)(x_shift * currentCellSize) + (int)x_shift,
				padding_y + (grid.getHeight() - s.getRow() - grid.getMinRow() - 1) * currentCellSize - (int)(y_shift * currentCellSize) - (int)y_shift
			) : null;
	}
	@Override //PointSpaceTranslator
	public int getCurrentCellSize() {return currentCellSize;}
	@Override //JComponent
	public Dimension getPreferredSize() {
		return new Dimension(
				getGridWidth(),
				getGridHeight()
			);
	}
	/** Draw the cells background images as
	 *  created by the cells render.  
	 * 
	 * @param g - the area on which to draw
	 */
	private void displayCells(Graphics2D g) {
		for(Space s : grid.allValidSpaces()) {
			Image i = cells.getCellBG(s, currentCellSize, currentCellSize);
			Point pos = spaceToPoint(s);
			g.drawImage(i, pos.x, pos.y, this);
		}
	}
	/** Draw the horizontal grid lines as created
	 *  by the lines render.  
	 * 
	 * @param g - the area on which to draw
	 */
	private void displayHorizontalLines(Graphics2D g) {
		for(int y = 1; y < grid.getHeight(); ++y) {
			Image i = lines.getHorizontalLine(y, getGridWidth());
			int pos = y * currentCellSize;
			if(i != null)
				pos -= i.getHeight(this)/2;
			g.drawImage(i, padding_x, pos + padding_y, this);
		}
	}
	/** Draw the vertical grid lines as created
	 *  by the lines render.  
	 * 
	 * @param g - the area on which to draw
	 */
	private void displayVerticalLines(Graphics2D g) {
		for(int x = 1; x < grid.getWidth(); ++x) {
			Image i = lines.getVerticalLine(x, getGridHeight());
			int pos = x * currentCellSize;
			if(i != null)
				pos -= i.getWidth(this)/2;
			g.drawImage(i, pos + padding_x, padding_y, this);
		}
	}
	/** Draw all of the occupants of the grid.  
	 * 
	 * @param g - the area on which to draw
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	private void displayOccupants(Graphics2D g) {
		Image image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
		Graphics2D draw = (Graphics2D)image.getGraphics();
		Collection<?> occupants = grid.getAll();
		for(Object current : occupants) {
			OccupantRender render = findRender(current);
			Animation<?> anim = getAnimation(current);
			Image i = render.getImage(current, currentCellSize, currentCellSize);
			if(anim == null) {
				Point p = spaceToPoint(grid.getCurrentSpace(current));
				g.drawImage(i, p.x - (i.getWidth(this) - currentCellSize) / 2, p.y - (i.getHeight(this) - currentCellSize) / 2, this);
			} else
				if(anim instanceof Translation)
					((Translation)anim).draw(i, draw, this, this);
				else if(anim instanceof AnimatedOccupantRender){
					Point p = spaceToPoint(grid.getCurrentSpace(current));
					Image i2 = ((AnimatedOccupantRender)anim).getNewImage(i, this);
					g.drawImage(i2, p.x - (i2.getWidth(this) - currentCellSize) / 2, p.y - (i2.getHeight(this) - currentCellSize) / 2, this);
				}
		} g.drawImage(image, 0, 0, this);
		draw.dispose();
	}
	/** Get the animation for the selected occupant.  
	 * 
	 * @param occupant - the occupant for which to get an animation
	 * @return - the animation (null if none are registered or occupant is null)
	 */
	private Animation<?> getAnimation(Object occupant) {
		if(occupant != null)
			for(Animation<?> a : animations)
				if(a.getOccupant() == occupant)
					return a;
		return null;
	}
	/** Finds the render set the the given object.  
	 *  If there is no render set for that object
	 *  type, the bor is returned.  
	 * 
	 * @param occupant - the object to get a render for
	 * @return - the appropriate render
	 */
	private OccupantRender<?> findRender(Object occupant) {
		Class<?> original = occupant.getClass();
		Class<?> current = original;
		while(true) {
			if(occupantRenders.containsKey(current)) {
				OccupantRender<?> r = occupantRenders.get(current);
				if(current != original)
					occupantRenders.put(original, r);
				return r; }
			Collection<Class<?>> interfaces = Arrays.asList(current.getInterfaces());
			Class<?> sup = current.getSuperclass();
			if(sup == null) {
				for(Class<?> i : interfaces)
					if(occupantRenders.containsKey(i)) {
						OccupantRender<?> r = occupantRenders.get(i);
						occupantRenders.put(original, r);
						return r;
					}
				occupantRenders.put(original, bor);
				return bor;
			} else {
				interfaces.removeAll(Arrays.asList(sup.getInterfaces()));
				for(Class<?> i : interfaces)
					if(occupantRenders.containsKey(i)) {
						OccupantRender<?> r = occupantRenders.get(i);
						occupantRenders.put(original, r);
						return r;
					}
				current = sup;
			}
		}
	}
	/** Test main methods used to test the main facilities of this board.  
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		final int FPS = 24;
		final int PADL = 10, PADR = 2, PADD = 1, PADU = 4;
		//create the window, grid, and display pane.  
		JFrame frame = new JFrame("TEST");
		Grid<Object> pieces = new RangeGrid<Object>(4 + PADL + PADR, 4 + PADD + PADU);
		final GridPane pane = new GridPane(pieces);
		//Create a render for the pieces
		pane.setOccupantRender(BasicPiece.class, new OccupantRenderAdapter<Piece>() {
			private Map<Color, Image> cache = new HashMap<Color, Image>();
			public Image getImage(Piece occupant, int width, int height) {
				Image image = cache.get(occupant.getColor());
				//if the cache is valid
				if(image != null && image.getWidth(getObserver()) == width && image.getHeight(getObserver()) == height)
					return image;
				//re-make the image
				image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
				Graphics draw = image.getGraphics();
				draw.setColor(occupant.getColor());
				draw.fillRect(width / 6, height / 6, 2 * width / 3, 2 * height / 3);
				draw.dispose();
				cache.put(occupant.getColor(), image);
				return image;
			}
		});
		//create a cell render for the grid
		pane.setCellRender(new CellRenderAdapter() {
			public Image getCellBG(Space space, int width, int height) {
				Image image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
				Graphics draw = image.getGraphics();
				int col = space.getColumn() % 2;
				int row = space.getRow() % 2;
				if(col != row) 
					draw.setColor(new Color(0x40C040)); //green
				else
					draw.setColor(new Color(0x404040)); //black
				draw.fillRect(0, 0, width, height);
				draw.dispose();
				return image;
			}
		});
		pane.setLineRender(new LineRenderAdapter() {
			@Override
			public boolean isVerticalFirst() {return false;}
			@Override
			public Image getHorizontalLine(int lineNumber, int width) {return null;}
			@Override
			public Image getVerticalLine(int lineNumber, int height) {return null;}
		});
		final Piece[] p = new Piece[4];
		int z = 0;
		for(int x = 1; x < 3; ++x)
			for(int y = 1; y < 3; ++y) {
				p[z] = new BasicPiece();
				p[z].putSetfOnGrid(pieces, Space.get(x + PADL, y + PADD));
				p[z++].setColor( x % 2 != y % 2 ? Color.red : Color.WHITE);
			}
		@SuppressWarnings("unchecked")
		final AnimatedOccupantRender<Piece>[] spinners = new AnimatedOccupantRender[4];
		final AnimationAutomater auto = new AnimationAutomater(1000 / FPS, pane);
		for(int x = 0; x < spinners.length; ++x) {
			spinners[x] = new SpinningOccupantRender<Piece>();
			spinners[x].setSpeed(FPS * 4);
			spinners[x].setOccupant(p[x]);
			pane.addAnimation(spinners[x]);
		}
		final AnimationGroup<Piece> anim = new AnimationGroup<Piece>(4);
		Runnable spinnerRunner = new Runnable() {
			public void run() {
				for(AnimatedOccupantRender<Piece> r : spinners) {
					anim.add(r);
					r.start();
				}
			}
		}; anim.setFinishTask(spinnerRunner);
		auto.addAnimationGroup(anim);
		final Piece mover = new BasicPiece();
		mover.putSetfOnGrid(pieces, Space.get(PADL, PADD));
		final Translation<Piece> transAnim = new LinearTranslation<Piece>();
		final SpinningOccupantRender<Piece> transSpinner = new SpinningOccupantRender<Piece>();
		transSpinner.setCounterClockwise(true);
		transSpinner.setSpeed(FPS);
		final CompoundAnimation<Piece> transComp = new CompoundAnimation<Piece>();
		transComp.setTranslation(transAnim);
		transComp.setRender(transSpinner);
		transComp.setOccupant(mover);
		pane.addAnimation(transComp);
		Runnable transRunner = new Runnable() {
			Direction d = Direction.LEFT;
			public void run() {
				d = Direction.fromDegrees(d.getDegreeValue() - 90);
				Space current = mover.getCurrentSpace();
				Space next = current.getSpaceInDirection(d, 3);
				mover.moveTo(next);
				transAnim.setTranslation(current, next, FPS);
				auto.addAnimation(transAnim);
				transSpinner.start();
				auto.addAnimation(transSpinner, this);
			}
		};
		frame.add(pane);
		frame.pack();
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setVisible(true);
		spinnerRunner.run();
		transRunner.run();
		auto.start();
	}
}
